import {InterpolateLinear, InterpolateSmooth, InterpolateDiscrete} from '../constants.js'
import {CubicInterpolant} from '../math/interpolants/CubicInterpolant.js'
import {LinearInterpolant} from '../math/interpolants/LinearInterpolant.js'
import {DiscreteInterpolant} from '../math/interpolants/DiscreteInterpolant.js'
import {AnimationUtils} from './AnimationUtils.js'

function KeyframeTrack(name, times, values, interpolation) {
  if (name === undefined) throw new Error('THREE.KeyframeTrack: track name is undefined')
  if (times === undefined || times.length === 0) throw new Error('THREE.KeyframeTrack: no keyframes in track named ' + name)

  this.name = name

  this.times = AnimationUtils.convertArray(times, this.TimeBufferType)
  this.values = AnimationUtils.convertArray(values, this.ValueBufferType)

  this.setInterpolation(interpolation || this.DefaultInterpolation)
}

// Static methods

Object.assign(KeyframeTrack, {
  // Serialization (in static context, because of constructor invocation
  // and automatic invocation of .toJSON):

  toJSON: function (track) {
    const trackType = track.constructor

    let json

    // derived classes can define a static toJSON method
    if (trackType.toJSON !== undefined) {
      json = trackType.toJSON(track)
    } else {
      // by default, we assume the data can be serialized as-is
      json = {
        name: track.name,
        times: AnimationUtils.convertArray(track.times, Array),
        values: AnimationUtils.convertArray(track.values, Array),
      }

      const interpolation = track.getInterpolation()

      if (interpolation !== track.DefaultInterpolation) {
        json.interpolation = interpolation
      }
    }

    json.type = track.ValueTypeName // mandatory

    return json
  },
})

Object.assign(KeyframeTrack.prototype, {
  constructor: KeyframeTrack,

  TimeBufferType: Float32Array,

  ValueBufferType: Float32Array,

  DefaultInterpolation: InterpolateLinear,

  InterpolantFactoryMethodDiscrete: function (result) {
    return new DiscreteInterpolant(this.times, this.values, this.getValueSize(), result)
  },

  InterpolantFactoryMethodLinear: function (result) {
    return new LinearInterpolant(this.times, this.values, this.getValueSize(), result)
  },

  InterpolantFactoryMethodSmooth: function (result) {
    return new CubicInterpolant(this.times, this.values, this.getValueSize(), result)
  },

  setInterpolation: function (interpolation) {
    let factoryMethod

    switch (interpolation) {
      case InterpolateDiscrete:
        factoryMethod = this.InterpolantFactoryMethodDiscrete

        break

      case InterpolateLinear:
        factoryMethod = this.InterpolantFactoryMethodLinear

        break

      case InterpolateSmooth:
        factoryMethod = this.InterpolantFactoryMethodSmooth

        break
    }

    if (factoryMethod === undefined) {
      const message = 'unsupported interpolation for ' + this.ValueTypeName + ' keyframe track named ' + this.name

      if (this.createInterpolant === undefined) {
        // fall back to default, unless the default itself is messed up
        if (interpolation !== this.DefaultInterpolation) {
          this.setInterpolation(this.DefaultInterpolation)
        } else {
          throw new Error(message) // fatal, in this case
        }
      }

      console.warn('THREE.KeyframeTrack:', message)
      return this
    }

    this.createInterpolant = factoryMethod

    return this
  },

  getInterpolation: function () {
    switch (this.createInterpolant) {
      case this.InterpolantFactoryMethodDiscrete:
        return InterpolateDiscrete

      case this.InterpolantFactoryMethodLinear:
        return InterpolateLinear

      case this.InterpolantFactoryMethodSmooth:
        return InterpolateSmooth
    }
  },

  getValueSize: function () {
    return this.values.length / this.times.length
  },

  // move all keyframes either forwards or backwards in time
  shift: function (timeOffset) {
    if (timeOffset !== 0.0) {
      const times = this.times

      for (let i = 0, n = times.length; i !== n; ++i) {
        times[i] += timeOffset
      }
    }

    return this
  },

  // scale all keyframe times by a factor (useful for frame <-> seconds conversions)
  scale: function (timeScale) {
    if (timeScale !== 1.0) {
      const times = this.times

      for (let i = 0, n = times.length; i !== n; ++i) {
        times[i] *= timeScale
      }
    }

    return this
  },

  // removes keyframes before and after animation without changing any values within the range [startTime, endTime].
  // IMPORTANT: We do not shift around keys to the start of the track time, because for interpolated keys this will change their values
  trim: function (startTime, endTime) {
    const times = this.times,
      nKeys = times.length

    let from = 0,
      to = nKeys - 1

    while (from !== nKeys && times[from] < startTime) {
      ++from
    }

    while (to !== -1 && times[to] > endTime) {
      --to
    }

    ++to // inclusive -> exclusive bound

    if (from !== 0 || to !== nKeys) {
      // empty tracks are forbidden, so keep at least one keyframe
      if (from >= to) {
        to = Math.max(to, 1)
        from = to - 1
      }

      const stride = this.getValueSize()
      this.times = AnimationUtils.arraySlice(times, from, to)
      this.values = AnimationUtils.arraySlice(this.values, from * stride, to * stride)
    }

    return this
  },

  // ensure we do not get a GarbageInGarbageOut situation, make sure tracks are at least minimally viable
  validate: function () {
    let valid = true

    const valueSize = this.getValueSize()
    if (valueSize - Math.floor(valueSize) !== 0) {
      console.error('THREE.KeyframeTrack: Invalid value size in track.', this)
      valid = false
    }

    const times = this.times,
      values = this.values,
      nKeys = times.length

    if (nKeys === 0) {
      console.error('THREE.KeyframeTrack: Track is empty.', this)
      valid = false
    }

    let prevTime = null

    for (let i = 0; i !== nKeys; i++) {
      const currTime = times[i]

      if (typeof currTime === 'number' && isNaN(currTime)) {
        console.error('THREE.KeyframeTrack: Time is not a valid number.', this, i, currTime)
        valid = false
        break
      }

      if (prevTime !== null && prevTime > currTime) {
        console.error('THREE.KeyframeTrack: Out of order keys.', this, i, currTime, prevTime)
        valid = false
        break
      }

      prevTime = currTime
    }

    if (values !== undefined) {
      if (AnimationUtils.isTypedArray(values)) {
        for (let i = 0, n = values.length; i !== n; ++i) {
          const value = values[i]

          if (isNaN(value)) {
            console.error('THREE.KeyframeTrack: Value is not a valid number.', this, i, value)
            valid = false
            break
          }
        }
      }
    }

    return valid
  },

  // removes equivalent sequential keys as common in morph target sequences
  // (0,0,0,0,1,1,1,0,0,0,0,0,0,0) --> (0,0,1,1,0,0)
  optimize: function () {
    // times or values may be shared with other tracks, so overwriting is unsafe
    const times = AnimationUtils.arraySlice(this.times),
      values = AnimationUtils.arraySlice(this.values),
      stride = this.getValueSize(),
      smoothInterpolation = this.getInterpolation() === InterpolateSmooth,
      lastIndex = times.length - 1

    let writeIndex = 1

    for (let i = 1; i < lastIndex; ++i) {
      let keep = false

      const time = times[i]
      const timeNext = times[i + 1]

      // remove adjacent keyframes scheduled at the same time

      if (time !== timeNext && (i !== 1 || time !== time[0])) {
        if (!smoothInterpolation) {
          // remove unnecessary keyframes same as their neighbors

          const offset = i * stride,
            offsetP = offset - stride,
            offsetN = offset + stride

          for (let j = 0; j !== stride; ++j) {
            const value = values[offset + j]

            if (value !== values[offsetP + j] || value !== values[offsetN + j]) {
              keep = true
              break
            }
          }
        } else {
          keep = true
        }
      }

      // in-place compaction

      if (keep) {
        if (i !== writeIndex) {
          times[writeIndex] = times[i]

          const readOffset = i * stride,
            writeOffset = writeIndex * stride

          for (let j = 0; j !== stride; ++j) {
            values[writeOffset + j] = values[readOffset + j]
          }
        }

        ++writeIndex
      }
    }

    // flush last keyframe (compaction looks ahead)

    if (lastIndex > 0) {
      times[writeIndex] = times[lastIndex]

      for (let readOffset = lastIndex * stride, writeOffset = writeIndex * stride, j = 0; j !== stride; ++j) {
        values[writeOffset + j] = values[readOffset + j]
      }

      ++writeIndex
    }

    if (writeIndex !== times.length) {
      this.times = AnimationUtils.arraySlice(times, 0, writeIndex)
      this.values = AnimationUtils.arraySlice(values, 0, writeIndex * stride)
    } else {
      this.times = times
      this.values = values
    }

    return this
  },

  clone: function () {
    const times = AnimationUtils.arraySlice(this.times, 0)
    const values = AnimationUtils.arraySlice(this.values, 0)

    const TypedKeyframeTrack = this.constructor
    const track = new TypedKeyframeTrack(this.name, times, values)

    // Interpolant argument to constructor is not saved, so copy the factory method directly.
    track.createInterpolant = this.createInterpolant

    return track
  },
})

export {KeyframeTrack}
